import {
  ComponentPropsOptions,
  ExtractPropTypes,
  SetupContext,
  defineComponent,
  getCurrentInstance,
  provide,
  inject
} from 'vue';

import {ComponentEvent, getComponentEmit, useEvent} from "./useEvent";

/** PropsOptions和Props两个泛型有什么作用*/
// const props = {
//   name: {type: String},
//   age: {type: Number, default: 10},
//   flag: {type: Boolean, required: true}
// }
//
// type PropsType = ExtractPropTypes<typeof props>
// /*
// type Props = {
//   age: number;
//   flag: boolean;
// } & {
//   name?: string | undefined;
// }
// * */
// const myProps:PropsType = {} as any
// //这样myProps就能 .到 age、flag、number三个属性,且被ts所限制
// myProps.

export function designComponent<
  PropsOptions extends Readonly<ComponentPropsOptions>,
  Props extends Readonly<ExtractPropTypes<PropsOptions>>,
  Emits extends { [k: string]: (...args: any[]) => boolean },
  Refer,
  >(
    options: {
      name?: string,
      props?: PropsOptions,
      setup: (parameter: { props: Props, event: ComponentEvent<Emits>, setupContext: SetupContext<Emits> }) => {
        refer?: Refer, //这个类型需要推断出来,因为我们并不知道开发者在定义他的组件的时候,他想要暴露什么东东出来
        render: () => any
      },
      provideRefer?: boolean,
      emits?: Emits //原生defineComponent({emits})中的emits
      mixins?,
      components?,
      directives?
    },
    expose?
  ) {


  const {setup, provideRefer, emits, ...leftOptions} = options;
  return {
    ...defineComponent({
      ...leftOptions,
      emits: getComponentEmit(emits),
      setup(props: Props, setupContext: SetupContext) {
        const ctx = getCurrentInstance() as any;
        const event = useEvent<Emits>(emits!)

        if (!setup) {
          console.error('designComponent: setup is required!');
          return () => null;
        }

        // @ts-ignore
        const {refer, render} = setup({props, setupContext, event}); //因为我们修改了designComponent的setup类型，原本参数是 (props,setupContext)，现在改成了 (parameter:{props,setupContext,event})，所以修改项目中已经使用了 designComponent的组件。通过解构的方式获取 props,setupContext。 //因为我们后续基本不使用 setupContext，所以这里通过解构的方式获取会方便一点；

        // ctx._refer = refer

        if (!!refer) {

          // 组件setup返回的对象里的key 不要和 组件接受的props对象里的key重名了
          const duplicateKey =
            Object.keys(leftOptions.props || {})
            .find(i => Object.prototype.hasOwnProperty.call(refer as any, i))

          if (!!duplicateKey) {
            console.error(`designComponent: duplicate key ${duplicateKey} in refer`)
          } else {
            // 我们直接将
            Object.assign(ctx.proxy/*即是我们的vm(被proxy包裹后的)*/, refer)
          }
        }
        console.log('ctx.proxy:',ctx.proxy)

        if (provideRefer) {
          if (!leftOptions.name) {
            console.error('designComponent: name is required when provideRefer is true!')
          } else {
            //把组件暴露出来的东西又放到provide里, 以便后代组件引用
            provide(`@@${leftOptions.name}`, refer); //同一组件的不同实例并不会覆盖, 是实例独享的, 会放在vm.$.provides下
          }
        }

        return render;
      }
    } as any),
    use: {
      ref: (refName: string) => {
        // const ctx = (getCurrentInstance() as any).ctx;
        // ↑ production模式下 getCurrentInstance().ctx 拿不到完整的vm, 里面只有一个属性 `_`, 也就是我们的 getCurrentInstance().ctx 内部对象自己
        // 故也就无法通过 getCurrentInstance().ctx 拿到 $refs

        // return {
        //   // value: (ctx as any).$refs[refName].$._refer as Refer | null
        //   //不应该在调用 XXX组件.use.ref() 确定这里返回的value↑的值
        //   //因为此时你想要获取的ref可能还没有值(还没有mounted(?))
        //   //故最好再用函数包一层, 在确保挂载好的时候, 通过动态获取
        //   //并且 我们这里不是将整个组件实例都返回了, 而只是返回了子组件主动暴露的引用们
        //
        //   get value() {
        //     return (ctx as any).$refs[refName].$/*或则_,是等价的,都是我们在组件内调用getCurrentInstance获取到的那个组件内部对象*/._refer as Refer | null;
        //   }
        // }

        const ctx = getCurrentInstance()!; /*此时获得的是父组件的实例,父组件实例下的refs属性存放着所有ref*/

        return {
          get value() {
            return ctx.refs[refName] as Refer | null
          }
        }
      },

      inject: (defaultValue?: any) => {
        return inject(`@@${leftOptions.name}`, defaultValue) as Refer;
      }
    },
    ...(expose||{})
  }
}

